from keras.datasets import mnist           
import matplotlib.pyplot as plt                                 
import numpy as np
from keras.layers import Dense                                  
from keras.models import Sequential                             
import tensorflow as tf

#Подрузка датасета сразу на обучение и на тест
(X_train, y_train), (X_test, y_test) = mnist.load_data()        

X_train.shape, X_test.shape

#Код для отрисовки картинок
# fig, ax = plt.subplots(1, 5, figsize=(15, 10))

# for i in range(5):
#     ax[i].imshow(X_train[i], cmap='gray')
#     ax[i].axis('off')

# y_train[:5]
# Данные на выходе 
# array([5, 0, 4, 1, 9], dtype=uint8



#Для обучения было взято 2 класса (цифры 0 и 1), вместо имеющихся 10, для простоты обучения нейросети
#Определение индексов необходимых значений
idxs = np.where((y_train == 0) | (y_train == 1))       
#Взятие целевых значений по этим индексам
y_train = y_train[idxs]
#Взятие признаков по обучению только по этим индексам
X_train = X_train[idxs] 

#Проверка новых размерностей
X_train.shape, y_train.shape
#Данные на выходе:
# ((12665, 28, 28), (12665,))
# Размерность датасета уменьшилась в 5 раз (из 10 цифр осталось только 2)
# Количество целевых значений y_train.shape осталось равным размерности датасета, идущего на обучение нейросети

#Тоже самое делается для тестовых данных
idxs = np.where((y_test == 0) | (y_test == 1))
y_test = y_test[idxs]

X_test = X_test[idxs]

X_test.shape, y_test.shape
#Данные на выходе:
# ((2115, 28, 28), (2115,))


#Проверка того, что у нас остались либо 0, либо 1
# fig, ax = plt.subplots(1, 5, figsize=(15, 10))

# for i in range(5):
#     ax[i].imshow(X_train[i], cmap='gray')
#     ax[i].axis('off')


#Проверка целевых значений
y_train[:5]

# Выходная информация
# array([0, 1, 1, 1, 1], dtype=uint8)


# Пиксели задаются двумя способами: числами от 0 до 255 и числами от 0 до 1
# Нормируем данные, сейчас обойдемся без MinMaxScaler из sklearn, а воспользуемся делением на 255,
# т.к. сейчас изображения представлены пикселями в диапазоне от 0 до 255, 
# а для нейросети комфортней обучаться на диапазоне от 0 до 1.

print(X_train.min(), X_train.max())
# Выходная информация
# 0 255

X_train = X_train / 255.0
X_test = X_test / 255.0

print(X_train.min(), X_train.max())
# Выходная информация
# 0.0 1.0



# Нейросеть на выходе будет выдавать вероятность принадлежности числа к нулю или единице
# В каждом нейроне будет происходить бинарная классификация
# Для этого из каждой метки 0 или 1 надо сделать преобразование в бинарный вид

from keras.utils.np_utils import to_categorical

y_train_cat = to_categorical(y_train)
y_test_cat = to_categorical(y_test)

#Вывод целевых значений
y_train[:5]
# Выходная информация
# array([0, 1, 1, 1, 1], dtype=uint8)

#После перевода целевых значений в бинарную составляющую появилось следующие значения
y_train_cat[:5]
# Выходная информация
# array([[1., 0.],
#        [0., 1.],
#        [0., 1.],
#        [0., 1.],
#        [0., 1.]], dtype=float32)



# А чтобы еще легче обучать нейронную сеть, поменяем масштаб изображений, 
# сейчас они 28 на 28, сделаем меньше, 6 на 6, чтобы нейросеть легче обучалась


#Создание фиктивной канальности для tensorflow, так как библиотека позволяет взаимодействовать только с цветными картинками
X_train[..., np.newaxis].shape
# Выходная информация
# (12665, 28, 28, 1)
# Тут по прежнему сохраняется прежняя размерность, но добавлен 1 канал


# Для облегчения веса пользуемся библиотекой tensorflow и её функцией resize, для изменения размера картинок
# Во время изменения размерности убираем фиктивный канал для цвета  - [..., 0]
X_train_resized = tf.image.resize(X_train[..., np.newaxis], (6, 6))[..., 0]
X_test_resized = tf.image.resize(X_test[..., np.newaxis], (6, 6))[..., 0]

#Проверка датасетов
# fig, ax = plt.subplots(1, 5, figsize=(15, 10))

# for i in range(5):
#     ax[i].imshow(X_train_resized[i], cmap='gray')
#     ax[i].axis('off')
#Датасеты в порядке




#
#Для того, что обучить нейронную сеть для любой задачи нужно ответить на три вопроса:

#Наша нейросеть пока что не умеет работать с двумерным входом,
#Поэтому изображения надо будет перевести в векторный формат из матрицы
#Для этого используется слой из keras Flatten, который вытягивает изображение в один вектор,
#Была картинка 6x6, а станет вектором с размерностью 36.

#Была матрица
X_train_resized[0].numpy()

#Данные на выходе представляют из себя матрицу(скобки квадратные)
# array([[0.        , 0.        , 0.        , 0.        , 0.        ,
#         0.        ],
#        [0.        , 0.        , 0.03594765, 0.9888889 , 0.42941174,
#         0.        ],
#        [0.        , 0.00228757, 0.7885626 , 0.        , 0.9901961 ,
#         0.        ],
#        [0.        , 0.6558824 , 0.        , 0.        , 0.88235295,
#         0.        ],
#        [0.        , 0.66078436, 0.5088234 , 0.72352946, 0.        ,
#         0.        ],
#        [0.        , 0.        , 0.        , 0.        , 0.        ,
#         0.        ]], dtype=float32)

X_train_resized[0].numpy().shape
# (6, 6)

#А теперь вектор
X_train_resized[0].numpy().flatten()        #Вызов метода flatten

#Данные на выходе преставляют из себя вектор
# array([0.        , 0.        , 0.        , 0.        , 0.        ,
#        0.        , 0.        , 0.        , 0.03594765, 0.9888889 ,
#        0.42941174, 0.        , 0.        , 0.00228757, 0.7885626 ,
#        0.        , 0.9901961 , 0.        , 0.        , 0.6558824 ,
#        0.        , 0.        , 0.88235295, 0.        , 0.        ,
#        0.66078436, 0.5088234 , 0.72352946, 0.        , 0.        ,
#        0.        , 0.        , 0.        , 0.        , 0.        ,
#        0.        ], dtype=float32)

#Проверка размерности
X_train_resized[0].numpy().flatten().shape
#(36,)



#Нейронная сеть состоит из двух нейронов
# Входные данные состоят из 36 пикселей. Каждый из пикселей прогоняется через 2 нейрона
# Каждый нейрон выдаёт вероятность
# Первый нейрон выдаёт вероятность быть нулевым классом
# Второй нейрон выдаёт вероятность быть первым классом
# По итогу сеть однослойная



# Чтобы не вносить датасет в предобработку, можно внести датасет в саму нейронную сеть
from keras.layers import Flatten   #Специальный слой для "Вытягивания вектора"
tf.random.set_seed(9)


# В нейронную сеть поступает 
model = Sequential([
    Flatten(input_shape=(6, 6)),
    Dense(2, activation='sigmoid')])

model.summary()

# Model: "sequential"
# _________________________________________________________________
#  Layer (type)                Output Shape              Param #   
# =================================================================
#  flatten (Flatten)           (None, 36)                0         
                                                                 
#  dense (Dense)               (None, 2)                 74        
                                                                 
# =================================================================
# Total params: 74
# Trainable params: 74
# Non-trainable params: 0

# На выходе имеем 74 параметра для обучения - в 2 нейрона идут 36 входов + 2 bias




# Для оптимизации классификации в данном случае не подойдёт функция потери mse
# Будет использоваться функция бинарной кросс - энтропии
# Метрика - accuracy
model.compile(optimizer='sgd', loss='binary_crossentropy', metrics='accuracy')

# Обучение модели
model.fit(X_train_resized, y_train_cat, epochs=5)

# Epoch 1/5
# 396/396 [==============================] - 1s 2ms/step - loss: 0.6321 - accuracy: 0.8079
# Epoch 2/5
# 396/396 [==============================] - 1s 2ms/step - loss: 0.4760 - accuracy: 0.9671
# Epoch 3/5
# 396/396 [==============================] - 1s 2ms/step - loss: 0.3888 - accuracy: 0.9747
# Epoch 4/5
# 396/396 [==============================] - 1s 2ms/step - loss: 0.3318 - accuracy: 0.9765
# Epoch 5/5
# 396/396 [==============================] - 1s 2ms/step - loss: 0.2918 - accuracy: 0.9768
# CPU times: user 4.08 s, sys: 215 ms, total: 4.3 s
# Wall time: 5.52 s
# <keras.callbacks.History at 0x7f6b6c44eb50>

# Потери уменьшаются, точность повышается, результат удовлетворительный

# С каждым шагом функция сходится к решению задачи. Свойство сходимости


# Теперь проверим, а как модель работает на новых данных.
# На выходе модель дает 2 вероятности:

# 1) Быть нулевым классом
# 2) Быть первым классом
# Для выбранного объекта вероятность быть первым классом гораздо выше, чем вероятность быть нулевым классом.


print("Предсказание нейронной сети: ")
pred = model.predict(X_test_resized[:1])
print(pred)

# Предсказание нейронной сети: 
# 1/1 [==============================] - 0s 313ms/step
# array([[0.2062995, 0.7718759]], dtype=float32)

print("Исходя из результатов, вероятность того, что на картинке единица, равняется 0.77")

#Индекс максимальной вероятности - 1 
pred_cls = pred.argmax()
pred_cls


print("Визуальная проверка предсказания")
idx = 0
plt.imshow(X_test_resized[idx])
plt.title(f'pred {pred_cls}, true {y_test[idx]}');

# На картинке размером 6 на 6 будет изображения единица